/*
 * Copyright (c) 2010 Google Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
 * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
 * License for the specific language governing permissions and limitations under
 * the License.
 */

package com.google.api.data;

import com.google.api.client.auth.oauth.OAuthHmacSigner;
import com.google.api.client.auth.oauth.OAuthParameters;
import com.google.api.client.googleapis.GoogleHeaders;
import com.google.api.client.googleapis.GoogleTransport;
import com.google.api.client.googleapis.GoogleUrl;
import com.google.api.client.googleapis.auth.clientlogin.ClientLogin;
import com.google.api.client.googleapis.auth.oauth.GoogleOAuthDomainWideDelegation;
import com.google.api.client.googleapis.xml.atom.AtomPatchRelativeToOriginalContent;
import com.google.api.client.googleapis.xml.atom.GData;
import com.google.api.client.http.HttpContent;
import com.google.api.client.http.HttpRequest;
import com.google.api.client.http.HttpResponseException;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.xml.XmlNamespaceDictionary;
import com.google.api.client.xml.atom.AtomContent;
import com.google.api.client.xml.atom.AtomParser;

import java.io.IOException;

/**
 * Generic service for the Google Data API.
 * 
 * @author Nicolas Garnier (nivco@google.com)
 * @since 2.2
 */
public class GenericAtomService implements AuthenticatedGenericService {

  // ============= Attributes ================

  /** The name of the application */
  protected String applicationName;

  /** HTTP Transport */
  protected HttpTransport transport;

  /** Connection headers */
  protected GoogleHeaders headers;

  /** XML Namespaces */
  protected XmlNamespaceDictionary namespaceDictionary;

  // ============= Constructors ================

  /**
   * Constructs a Google API service for an Atom based API.
   */
  public GenericAtomService(String applicationName, XmlNamespaceDictionary namespaceDictionary) {
    this.applicationName = applicationName;
    transport = GoogleTransport.create();
    headers = (GoogleHeaders) transport.defaultHeaders;
    headers.setApplicationName(applicationName);
    this.namespaceDictionary = namespaceDictionary;
    AtomParser parser = new AtomParser();
    parser.namespaceDictionary = namespaceDictionary;
    transport.addParser(parser);
  }

  // ============= Mandatory Setup Methods ================

  public void setApiVersion(String apiVersion) {
    headers.gdataVersion = apiVersion;
  }

  // =============== Authentication ================

  public void setClientLoginCredentials(String username, String password, String serviceCode)
      throws HttpResponseException, IOException {
    setClientLoginCredentials(username, password, AccountType.HOSTED_OR_GOOGLE, serviceCode);
  }

  public void setClientLoginCredentials(String username, String password, AccountType accountType,
      String serviceCode) throws HttpResponseException, IOException {
    ClientLogin authenticator = new ClientLogin();
    authenticator.applicationName = applicationName;
    authenticator.authTokenType = serviceCode;
    authenticator.username = username;
    authenticator.password = password;
    // TODO(nivco): Remove comments when this is implemented
    // authenticator.accountType = accountType.toString();
    authenticator.authenticate().setAuthorizationHeader(transport);
  }

  /**
   * Set the ClientLogin authentication token for the service.
   * 
   * @param authToken The ClientLogin authentication token to use.
   */
  public void setClientLoginAuthenticationToken(String authToken) {
    transport.defaultHeaders.authorization = GoogleHeaders.getGoogleLoginValue(authToken);
  }

  /**
   * Sets the OAuth credentials used to generate the authorization header. This
   * header needs to be set per request, as it depends on the request url. The
   * following OAuth parameters are required:
   * <ul>
   * <li>oauth_consumer_key
   * <li>oauth_token
   * </ul>
   * 
   * @param parameters the OAuth parameters to use to generated the header
   * @param signer the signing method to use for signing the header
   */
  public void setOAuthCredentials(String consumerKey, String consumerSecret, String requestorId) {
    GoogleOAuthDomainWideDelegation googleOAuthDomainWideDelegation = new GoogleOAuthDomainWideDelegation();
    OAuthHmacSigner signer = new OAuthHmacSigner();
    OAuthParameters oauthParameters = new OAuthParameters();

    googleOAuthDomainWideDelegation.requestorId = requestorId;
    signer.clientSharedSecret = consumerSecret;
    oauthParameters.signer = signer;
    oauthParameters.consumerKey = consumerKey;
    googleOAuthDomainWideDelegation.signRequests(transport, oauthParameters);
  }

  // ============= API Requests Methods ================

  /**
   * Gets the given {@code url} and matches the results to the given
   * {@code feedClass} Type. Returns {@code null} if the etag matches.
   * 
   * @param url The URL of the Feed or Entry to get.
   * @param etag etag of the current version of the entry to get, this does a
   *          conditional get, {@code null} will be the response if the given
   *          etag is the one of the current server. If {@code null} this will
   *          be a normal/non-conditional Get.
   * @param doPartialGet true if we should only request the fields that are
   *          described in the {@code feedClass}. This uses the {@code fields}
   *          GData URL parameter.
   * @param feedClass The Class to which the responses values must me matched,
   *          this will also be the type of the response.
   * @return An instance of the {@code feedClass} Class with the response values
   *         mapped to its fields.
   * @throws IOException
   */
  public <FeedOrEntryType> FeedOrEntryType executeGet(GoogleUrl url, String etag,
      boolean doPartialGet, Class<? extends FeedOrEntryType> feedClass) throws IOException {
    checkGoogleTransport();
    if (doPartialGet) {
      url.fields = GData.getFieldsFor(feedClass);
    }
    HttpRequest request = transport.buildGetRequest();
    request.url = url;
    request.headers.ifNoneMatch = etag;
    return request.execute().parseAs(feedClass);
  }

  /**
   * Inserts the given {@code entry} to the given {@code feedPostUrl}.
   * 
   * @param entry
   * @param feedPostUrl
   * @return
   * @throws IOException
   */
  @SuppressWarnings("unchecked")
  public <EntryType> EntryType executeInsert(EntryType entry, GoogleUrl feedPostUrl)
      throws IOException {
    AtomContent content = new AtomContent();
    content.namespaceDictionary = namespaceDictionary;
    content.entry = entry;
    return (EntryType) executeInsert(content, feedPostUrl, entry.getClass());
  }

  /**
   * Inserts the given {@code content} to the given {@code feedPostUrl}.
   * 
   * @param content
   * @param feedPostUrl
   * @param feedClass
   * @return
   * @throws IOException
   */
  protected <FeedOrEntryType> FeedOrEntryType executeInsert(HttpContent content,
      GoogleUrl feedPostUrl, Class<? extends FeedOrEntryType> feedClass) throws IOException {
    checkGoogleTransport();
    checkNamespaceDict();
    checkAltIsAtom(feedPostUrl);
    HttpRequest request = transport.buildPostRequest();
    request.url = feedPostUrl;
    request.content = content;
    return (FeedOrEntryType) request.execute().parseAs(feedClass);
  }

  /**
   * Updates the given {@code entry} and posts it to the given
   * {@code entryEditUrl}.
   * 
   * @param entry
   * @param entryEditUrl
   * @return
   * @throws IOException
   */
  @SuppressWarnings("unchecked")
  public <EntryType> EntryType executeUpdate(EntryType entry, GoogleUrl entryEditUrl, String etag)
      throws IOException {
    checkGoogleTransport();
    checkNamespaceDict();
    checkAltIsAtom(entryEditUrl);
    HttpRequest request = transport.buildPutRequest();
    request.url = entryEditUrl;
    request.headers.ifMatch = etag;
    AtomContent content = new AtomContent();
    content.namespaceDictionary = namespaceDictionary;
    content.entry = entry;
    request.content = content;
    return (EntryType) request.execute().parseAs(entry.getClass());
  }

  /**
   * Deletes the entry defined by the given {@code entryEditLink}.
   * 
   * @param entryEditLink The edit link of the Entry
   * @param etag The etag that the entry has to match to get deleted or
   *          {@code *} to delete independently of the version.
   * @throws IOException
   */
  public void executeDelete(GoogleUrl entryEditLink, String etag) throws IOException {
    checkGoogleTransport();
    HttpRequest request = transport.buildDeleteRequest();
    request.url = entryEditLink;
    request.headers.ifMatch = etag;
    request.execute().ignore();
  }

  /**
   * Only sends out the differences between the given {@code updated} and
   * {@code original} entries.
   * 
   * @param updated
   * @param original
   * @param entryEditLink
   * @param etag
   * @return
   * @throws IOException
   */
  @SuppressWarnings("unchecked")
  public <EntryType> EntryType executePatchRelativeToOriginal(EntryType updated,
      EntryType original, GoogleUrl entryEditLink, String etag) throws IOException {
    checkGoogleTransport();
    checkNamespaceDict();
    checkAltIsAtom(entryEditLink);
    HttpRequest request = transport.buildPatchRequest();
    request.url = entryEditLink;
    request.headers.ifMatch = etag;
    AtomPatchRelativeToOriginalContent content = new AtomPatchRelativeToOriginalContent();
    content.namespaceDictionary = namespaceDictionary;
    content.originalEntry = original;
    content.patchedEntry = updated;
    request.content = content;
    return (EntryType) request.execute().parseAs(original.getClass());
  }

  /**
   * Checks if the Google Transport has been set and, if not, throws a
   * {@code NullPointerException}.
   */
  protected void checkGoogleTransport() {
    if (transport == null) {
      throw new NullPointerException("transport is null. Please set the transport.");
    }
  }

  /**
   * Checks if the XML Namespace Dictionary has been set and, if not, throws a
   * {@code NullPointerException}.
   */
  protected void checkNamespaceDict() {
    if (namespaceDictionary == null) {
      throw new NullPointerException("namespaceDict is null. Please set the namespaceDict.");
    }
  }

  /**
   * Makes sure the "alt" URL parameter is "atom".
   * 
   * @param url The GoogleURL to change.
   */
  protected void checkAltIsAtom(GoogleUrl url) {
    url.alt = "atom";
  }

  // ============= Accessors ================

  public HttpTransport getTransport() {
    return transport;
  }

  public GoogleHeaders getHeaders() {
    return headers;
  }

  public XmlNamespaceDictionary getNamespaceDict() {
    return namespaceDictionary;
  }

}
